Add PYTHAINLP_OFFLINE support, is_offline_mode(), and PYTHAINLP_DATA env var to corpus path handling - #1309
Conversation
…g in callers Co-authored-by: bact <128572+bact@users.noreply.github.com>
…t method, update docstring Co-authored-by: bact <128572+bact@users.noreply.github.com>
get_corpus_path(); raise errors in callers
|
Co-authored-by: bact <128572+bact@users.noreply.github.com>
Done in commit
|
get_corpus_path(); raise errors in callersPYTHAINLP_OFFLINE support to get_corpus_path(); raise errors in callers
|
… deprecation Co-authored-by: bact <128572+bact@users.noreply.github.com>
Done in commit
|
PYTHAINLP_OFFLINE support to get_corpus_path(); raise errors in callersPYTHAINLP_OFFLINE support, is_offline_mode(), and PYTHAINLP_DATA env var to corpus path handling
wannaphong
left a comment
There was a problem hiding this comment.
We can use
$ thainlp data get
to download cropus.
| ] | ||
| raise FileNotFoundError( | ||
| f"Corpus file(s) not found: {', '.join(missing)}. " | ||
| f"To download, run: pythainlp.corpus.download('<name>') " |
There was a problem hiding this comment.
I think we should add
$ thainlp data get
too.
| # get the model, download it if it's not available locally | ||
| self.__model_filename: str = get_corpus_path(_MODEL_NAME) # type: ignore[assignment] | ||
| if not self.__model_filename: | ||
| raise FileNotFoundError( |
There was a problem hiding this comment.
I think we should add
$ thainlp data get
too.
| if not self.checkpoint: | ||
| raise FileNotFoundError( | ||
| f"Corpus '{_MODEL_NAME}' not found. " | ||
| f"To download: pythainlp.corpus.download('{_MODEL_NAME}', version='0.2')" |
There was a problem hiding this comment.
I think we should add
$ thainlp data get
too.
| # get the model, download it if it's not available locally | ||
| self.__model_filename = get_corpus_path(_MODEL_NAME) # type: ignore[assignment] | ||
| if not self.__model_filename: | ||
| raise FileNotFoundError( |
There was a problem hiding this comment.
I think we should add
$ thainlp data get
too.
| if not path_model: | ||
| raise FileNotFoundError( | ||
| "Corpus 'thai_nner' not found. " | ||
| "To download: pythainlp.corpus.download('thai_nner')" |
There was a problem hiding this comment.
I think we should add
$ thainlp data get
too.
| # Load bigram dictionary from downloaded corpus | ||
| bigram_path = get_corpus_path(_BIGRAM_CORPUS_NAME) | ||
| if not bigram_path: | ||
| raise FileNotFoundError( |
There was a problem hiding this comment.
I think we should add
$ thainlp data get
too.
| raise FileNotFoundError( | ||
| f"Corpus '{name}' not found locally. " | ||
| f"PYTHAINLP_OFFLINE is set; automatic downloading is disabled. " | ||
| f"To download, unset PYTHAINLP_OFFLINE and run: " |
There was a problem hiding this comment.
I think we should add
$ thainlp data get
too.
| f"Corpus '{name}' expected at '{path}' but file not found. " | ||
| f"PYTHAINLP_OFFLINE is set; automatic re-downloading is disabled. " | ||
| f"To re-download, unset PYTHAINLP_OFFLINE and run: " | ||
| f"pythainlp.corpus.download('{name}', force=True)" |
There was a problem hiding this comment.
I think we should add
$ thainlp data get
too.
|
…o all error messages Co-authored-by: bact <128572+bact@users.noreply.github.com>
|
|
@wannaphong added command line suggestions |



get_corpus_path()silently triggered network downloads whenever a corpus file was missing, making behavior unpredictable in web APIs and other non-interactive contexts. There was also no way for operators to disable automatic downloading at deployment time, no public API to check offline status, and the data directory env var did not follow established conventions.What was wrong
get_corpus_path()calleddownload()unconditionally whenever a corpus file was missing — whether or not the caller wanted a download, and with no mechanism to prevent it in production deployments. There was no publicis_offline_mode()helper, and the data directory was configured viaPYTHAINLP_DATA_DIR(inconsistent with theNLTK_DATAconvention used by similar libraries). Corpus-not-found error messages were inconsistent across callers, used different exception types, and gave no CLI download hint.How this fixes it
PYTHAINLP_OFFLINEenvironment variable (same semantics asHF_HUB_OFFLINE):PYTHAINLP_OFFLINEis set to a truthy value (e.g."1"),get_corpus_path()raisesFileNotFoundErrorimmediately when the corpus is not cached locally — no network call is made.PYTHAINLP_OFFLINEis unset or falsy,get_corpus_path()auto-downloads as before (backward-compatible default).download()— whether called from Python code or thethainlp data getCLI — always executes regardless ofPYTHAINLP_OFFLINE. An explicit call todownload()is a deliberate user action and must not be blocked.PYTHAINLP_OFFLINEonly prevents the automatic download triggered internally byget_corpus_path().pythainlp.is_offline_mode()public API (mirrorshuggingface_hub.is_offline_mode()):pythainlp/tools/path.py, exported frompythainlp.toolsand from the top-levelpythainlppackage.download()calls.PYTHAINLP_DATAenvironment variable (followsNLTK_DATApattern):PYTHAINLP_DATAis now the preferred env var for overriding the data directory.PYTHAINLP_DATA_DIRis still accepted for backward compatibility but emits aDeprecationWarning.ValueErroris raised to prevent silent misconfiguration.PYTHAINLP_DATA.Consistent, machine-parsable error messages across all callers:
FileNotFoundErrornow follows a uniform format with both Python API and CLI download hints:corpus/core.py,transliterate/(thai2rom,thaig2p,thai2rom_onnx,w2p),tag/(thainer,thai_nner,unigram,perceptron),spell/symspellpy,word_vector/core,augment/word2vec/(ltw2v,thai2fit),generate/thai2fit,ulmfit/core.RuntimeErrorandValueErrorcorpus-not-found errors converted toFileNotFoundErrorfor consistency.Refactoring to reduce cognitive complexity:
is_offline_mode()(public) from the former private_is_offline()helper._resolve_corpus_file_path()helper to flatten the nested folder/file branching insideget_corpus_path(). Cognitive complexity reduced to ~12 (below the target of 15).Caller updates (safety nets for when download fails or path is unresolvable):
thai2rom,thaig2p,thai2rom_onnx,word_vector/core,spell/symspellpy) now raiseFileNotFoundErrorwith a download instruction when the path is falsy.transliterate/w2p.pyhad its own inline auto-download pattern; replaced withFileNotFoundError.is Noneguards updated tonot path:thainer,unigram,perceptron,thai_nner,en_th,ulmfit/core,ltw2v,thai2fit.Test updates:
test_get_corpus_path_offline_modecovers all three behaviors:PYTHAINLP_OFFLINE=1raises for missing corpus,PYTHAINLP_OFFLINE=1raises for registered-but-missing file, and file present returns path normally.test_download_ignores_offline_modeverifiesdownload()succeeds withPYTHAINLP_OFFLINE=1.test_custom_data_dir_newverifiesPYTHAINLP_DATAis respected.test_custom_data_dirverifiesPYTHAINLP_DATA_DIRstill works but emitsDeprecationWarning.test_custom_data_dir_conflictverifies both set simultaneously raisesValueError.test_is_offline_modeverifies all truthy/falsy values ofPYTHAINLP_OFFLINE.test_zipnow explicitly downloads before asserting path existence.TagTestCase,PerceptronTaggerTestCase, andGenerateTestCaseeach gain asetUpClassthat downloads their required corpora.Your checklist for this pull request
Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.